﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using YawetagLib.Reflection;
using YawetagLib.Utilities;

namespace YawetagLib.Linq
{
    public static class Extensions
    {
        internal static Type GetSequenceElementType(this Type type)
        {
            Type enumerable = GetEnumerable(type);

            return enumerable != null ? enumerable.GetGenericArguments()[0] : type;
        }

        private static Type GetEnumerable(Type type)
        {
            if (type != null && type != typeof (string))
            {
                if (type.IsArray)
                {
                    return typeof(IEnumerable<>).MakeGenericType(type.GetElementType());
                }

                if (type.IsGenericType)
                {
                    foreach (Type arg in type.GetGenericArguments())
                    {
                        Type enumerable = typeof (IEnumerable<>).MakeGenericType(arg);
                        if (enumerable.IsAssignableFrom(type)) { return enumerable; }
                    }
                }

                Type[] interfaces = type.GetInterfaces();
                if (interfaces.Length > 0)
                {
                    foreach (Type iface in interfaces)
                    {
                        Type enumerable = GetEnumerable(iface);
                        if (enumerable != null) { return enumerable; }
                    }
                }

                if (type.BaseType != null && type.BaseType != typeof (object))
                {
                    return GetEnumerable(type.BaseType);
                }

                return null;
            }

            return null;
        }

        public static IQueryable<TSource> Sort<TSource>(this IQueryable<TSource> source, string sortExpression)
        {
            if (!string.IsNullOrEmpty(sortExpression))
            {
                IEnumerable<SortItem> sortItems = GetSortItems(sortExpression);
                AccessorDef[] accessorDefs = GetAccessorDefs(sortItems);
                ProjectionInfo projectionInfo = source.ProjectAndGetType(accessorDefs);

                IQueryable result = (IQueryable)typeof(Extensions)
                    .GetMethod("SortIteratorHelper", BindingFlags.Static | BindingFlags.NonPublic)
                    .MakeGenericMethod(projectionInfo.Type)
                    .Invoke(null, new object[] { projectionInfo.Result, sortItems });

                return result.Project<TSource>(projectionInfo.Type, accessorDefs[0].PropertyName);
            }

            return source;
        }

        public static IQueryable Project<TSource>(this IQueryable<TSource> source, params AccessorDef[] accessorDefs)
        {
            return source.ProjectAndGetType(accessorDefs).Result;
        }

        public static ProjectionInfo ProjectAndGetType<TSource>(this IQueryable<TSource> source, params AccessorDef[] accessorDefs)
        {
            string[] propertyNames = accessorDefs.Select(i => i.PropertyName).ToArray();
            Type[] argumentTypes = accessorDefs.Select(i => typeof(TSource).GetPropertyType(i)).ToArray();
            Type anonymousType = new AnonymousBuilder(propertyNames).GetBuiltType().MakeGenericType(argumentTypes);

            ParameterExpression parameter = Expression.Parameter(typeof(TSource), "i");
            Expression[] arguments = accessorDefs.Select(i => parameter.GetPropertyExpression(i)).ToArray();
            MemberBinding[] bindings = anonymousType.GetProperties().Select((i, index) => Expression.Bind(i, arguments[index])).ToArray();
            Expression body = Expression.MemberInit(Expression.New(anonymousType.GetConstructor(Type.EmptyTypes)), bindings);

            Expression<Func<TSource, object>> selectExpression = Expression.Lambda<Func<TSource, object>>(body, parameter);

            return ProjectionInfo.New(anonymousType, source.Select(selectExpression));
        }

        public static IQueryable<TResult> Project<TResult>(this IQueryable source, Type sourceType, string propertyName)
        {
            return (IQueryable<TResult>)typeof(Extensions)
                .GetMethod("ProjectHelper", BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(sourceType, typeof(TResult))
                .Invoke(null, new object[] { source, propertyName });
        }

        private static IQueryable<TSource> GetQueryableByID<TSource, TKey>(this IQueryable<TSource> source, 
            TKey id, Func<PropertyInfo, bool> isPrimaryKey)
        {
            ParameterExpression parameter = Expression.Parameter(typeof(TSource), "i");
            Expression body = Expression.Equal(Expression.Property(parameter, typeof(TSource).GetPrimaryKey<TKey>(isPrimaryKey)), Expression.Constant(id));

            Expression<Func<TSource, bool>> whereExpression = Expression.Lambda<Func<TSource, bool>>(body, parameter);

            return source.Where(whereExpression);
        }

        public static TSource GetByID<TSource, TKey>(this IQueryable<TSource> source, TKey id, Func<PropertyInfo, bool> isPrimaryKey)
        {
            return source.GetQueryableByID(id, isPrimaryKey).Single();
        }

        public static TSource GetOrDefaultByID<TSource, TKey>(this IQueryable<TSource> source, TKey id, Func<PropertyInfo, bool> isPrimaryKey)
        {
            return source.GetQueryableByID(id, isPrimaryKey).SingleOrDefault();
        }

        private static IEnumerable<SortItem> GetSortItems(string sortExpression)
        {
            IList<SortItem> sortItems = new List<SortItem>();

            foreach (string part in sortExpression.Split(',').Select(i => i.InnerTrim()))
            {
                string accessor = part.Split(' ')[0];
                bool isFirst = sortItems.Count == 0;
                bool isDescending = part.Split(' ').Length == 2 && part.Split(' ')[1].ToLower().Contains("desc");

                sortItems.Add(new SortItem { Accessor = accessor, IsFirst = isFirst, IsDescending = isDescending });
            }

            return sortItems.ToArray();
        }

        private static AccessorDef[] GetAccessorDefs(IEnumerable<SortItem> sortItems)
        {
            return (new[] { AccessorDef.New() })
                .Concat(sortItems.Select(i => AccessorDef.New(i.PropertyName, i.Accessor)))
                .ToArray();
        }

        internal static IQueryable<TSource> SortIteratorHelper<TSource>(IQueryable source, SortItem[] sortItems)
        {
            IQueryable<TSource> result = source.OfType<TSource>();

            foreach (SortItem sortItem in sortItems)
            {
                Type keyType = typeof(TSource).GetProperty(sortItem.PropertyName).PropertyType;

                result = (IQueryable<TSource>)typeof(Extensions)
                    .GetMethod("SortHelper", BindingFlags.Static | BindingFlags.NonPublic)
                    .MakeGenericMethod(typeof(TSource), keyType)
                    .Invoke(null, new object[] { result, sortItem });
            }

            return result;
        }

        internal static IQueryable<TSource> SortHelper<TSource, TKey>(IQueryable<TSource> source, SortItem sortItem)
        {
            ParameterExpression parameter = Expression.Parameter(typeof(TSource), "i");
            Expression body = Expression.Property(parameter, sortItem.PropertyName);

            Expression<Func<TSource, TKey>> orderByExpression = Expression.Lambda<Func<TSource, TKey>>(body, parameter);

            return sortItem.IsFirst && !sortItem.IsDescending
                ? source.OrderBy(orderByExpression)
                    : sortItem.IsFirst && sortItem.IsDescending
                ? source.OrderByDescending(orderByExpression)
                    : !sortItem.IsFirst && !sortItem.IsDescending
                ? ((IOrderedQueryable<TSource>)source).ThenBy(orderByExpression)
                : ((IOrderedQueryable<TSource>)source).ThenByDescending(orderByExpression);
        }

        internal static IQueryable<TResult> ProjectHelper<TSource, TResult>(IQueryable source, string propertyName)
        {
            ParameterExpression parameter = Expression.Parameter(typeof(TSource), "i");
            Expression body = Expression.Property(parameter, propertyName);

            Expression<Func<TSource, TResult>> selectExpression = Expression.Lambda<Func<TSource, TResult>>(body, parameter);

            return source.OfType<TSource>().Select(selectExpression);
        }

        private static Type GetPropertyType(this Type type, AccessorDef accessorDef)
        {
            if (!accessorDef.IsSelfReferenced)
            {
                foreach (PropertyInfo pi in type.GetProperties().Where(i => accessorDef.FirstPart.Equals(i.Name)))
                {
                    return accessorDef.IsNested ? pi.PropertyType.GetPropertyType(accessorDef.NextDefinition) : pi.PropertyType;
                }

                throw new Exception(string.Format("There is no property accessor {0} inside the class {1}",
                    accessorDef.Accessor, type.Name));
            }

            return type;
        }

        private static Expression GetPropertyExpression(this Expression expression, AccessorDef accessorDef)
        {
            if (!accessorDef.IsSelfReferenced)
            {
                MemberExpression pe = Expression.Property(expression, accessorDef.FirstPart);

                return accessorDef.IsNested ? pe.GetPropertyExpression(accessorDef.NextDefinition) : pe;
            }

            return expression;
        }

        private static PropertyInfo GetPrimaryKey<T>(this Type entityType, Func<PropertyInfo, bool> isPrimaryKey)
        {
            foreach (PropertyInfo pInfo in entityType.GetProperties().Where(isPrimaryKey))
            {
                if (pInfo.PropertyType != typeof(T))
                {
                    throw new ApplicationException(string.Format("Primary key, '{0}', of type '{1}' is not int", pInfo.Name, entityType));
                }

                return pInfo;
            }

            throw new ApplicationException(string.Format("No primary key defined for type {0}", entityType.Name));
        }

        internal struct SortItem
        {
            public string Accessor { get; set; }

            public string PropertyName
            {
                get { return Accessor.Replace(".", "_"); }
            }

            public bool IsFirst { get; set; }
            public bool IsDescending { get; set; }
        }
    }
}
